package de.mmis.core.tuplespace.gui;

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import org.apache.log4j.Logger;

import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.device.Device;
import de.mmis.core.base.event.AbstractTypedEvent;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.sexpression.SExpressionWriter;
import de.mmis.core.tuplespace.Notification;
import de.mmis.core.tuplespace.Notification.Command;
import de.mmis.core.tuplespace.Tuplespace;
import de.mmis.core.tuplespace.TuplespaceEvent;
import de.mmis.core.tuplespace.TuplespaceException;

/**
 * A Panel containing a {@link JList} showing all Tuples in a given
 * {@link Tuplespace} matching a specified Pattern.
 * 
 * @author Martin Nyolt
 * 
 */
public class TuplespacePanel extends JPanel implements
		Observable<TuplespacePanel.TuplespacePanelEvent> {

	private final static Logger LOGGER = Logger
			.getLogger(TuplespacePanel.class);

	private static final long serialVersionUID = -4571496304068014471L;

	public static class TuplespacePanelEvent extends
			AbstractTypedEvent<TuplespacePanelEvent.TYPE> {

		enum TYPE {
			/**
			 * the user has selected a tuple from the list, the tuple is stored
			 * in expression
			 */
			TUPLE_SELECTED,

			/** a tuple has been added or removed */
			SPACE_CHANGED;
		}

		/** only valid if the type is TUPLE_SELECTED */
		public Tree expression;

		private TuplespacePanelEvent(TYPE type) {
			super(type);
		}

		public static TuplespacePanelEvent tupleSelected(Tree expr) {
			TuplespacePanelEvent result = new TuplespacePanelEvent(
					TYPE.TUPLE_SELECTED);
			result.expression = expr;
			return result;
		}

		public static TuplespacePanelEvent spaceChanged() {
			return new TuplespacePanelEvent(TYPE.SPACE_CHANGED);
		}
	}

	private JList list_tuples;
	private DefaultListModel listModel_tuple;

	private Device<Tuplespace> space;
	private LinkedList<Notification> notifications = new LinkedList<Notification>();
	private LinkedList<Observer<TuplespacePanelEvent>> observers = new LinkedList<Observer<TuplespacePanelEvent>>();

	public TuplespacePanel(final Device<Tuplespace> space)
			throws TuplespaceException {
		setTuplespace(space);

		GridBagLayout layout = new GridBagLayout();
		layout.rowWeights = new double[] { 1.0 };
		layout.rowHeights = new int[] { 1 };
		layout.columnWeights = new double[] { 1.0 };
		layout.columnWidths = new int[] { 1 };
		setLayout(layout);

		listModel_tuple = new DefaultListModel();
		list_tuples = new JList(listModel_tuple);
		list_tuples.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent evt) {
				if (list_tuples.getSelectedValue() != null
						&& list_tuples.getSelectedValue() instanceof Tree) {
					TuplespacePanelEvent event = TuplespacePanelEvent
							.tupleSelected((Tree) list_tuples
									.getSelectedValue());
					fireEvent(event);
				}
			}
		});

		add(list_tuples, new GridBagConstraints(0, 0, 1, 1, 0, 0,
				GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
						1, 1, 1, 1), 0, 0));
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		if (space != null) {
			space.ensureAvailability();
			try {
				space.getDevice().unnotifyAll();
			} finally {
				space.releaseAvailability();
			}
		}
	}

	/**
	 * use a new {@link Tuplespace}. <b>null</b> will disable this Panel.
	 * 
	 * @param space
	 * @throws TuplespaceException
	 */
	public void setTuplespace(Device<Tuplespace> space)
			throws TuplespaceException {
		if (this.space != null) {
			try {
				this.space.ensureAvailability();
				this.space.getDevice().unnotifyAll();
			} catch (TuplespaceException e) {
				// ignore
				LOGGER.warn(e, e);
			} finally {
				this.space.releaseAvailability();
			}
		}

		this.space = space;
		if (space == null)
			return;

		space.ensureAvailability();
		space.getDevice().addObserver(new Observer<TuplespaceEvent>() {
			@Override
			public void notify(Observable<? extends TuplespaceEvent> sender,
					TuplespaceEvent event) {
				try {
					refresh();
					fireEvent(TuplespacePanelEvent.spaceChanged());
				} catch (TuplespaceException e) {
					LOGGER.error("error refreshing TuplespacePanel", e);
				}
			}
		});

		for (Notification notification : notifications) {
			try {
				space.getDevice().notify(notification);
			} catch (TuplespaceException e) {
				notification = null;
				space.releaseAvailability();
				throw e;
			}
			refresh();
		}

		space.releaseAvailability();
	}

	/**
	 * set a new pattern, removing all existing patterns; the list is updated
	 * automatically. If the pattern is <b>null</b>, all tuples are shown, but
	 * not updated automatically if a tuple changes.
	 * 
	 * @param pattern
	 * 
	 * @throws TuplespaceException
	 *             when the new {@link Notification} could not be set.
	 * 
	 * @see #addPattern(Tree)
	 */
	public void setPattern(Tree pattern) throws TuplespaceException {
		// for notifications
		// unnotify old patterns --> list should contain only SExpressions
		if (space != null)
			try {
				space.ensureAvailability();
				space.getDevice().unnotifyAll();
			} catch (TuplespaceException e) {
				// ignore
				LOGGER.warn(e, e);
			} finally {
				space.releaseAvailability();
			}

		notifications.clear();

		// set notification-template to new pattern

		addPattern(pattern);
	}

	public void addPattern(Tree pattern) throws TuplespaceException {
		Notification notification;
		if (pattern != null)
			notifications.add(notification = new Notification(Command.BOTH,
					pattern));
		else
			notification = null;

		if (space != null)
			try {
				space.ensureAvailability();
				if (notification != null)
					space.getDevice().notify(notification);
				refresh();
			} catch (TuplespaceException e) {
				notification = null;
				throw e;
			} finally {
				space.releaseAvailability();
			}
	}

	/**
	 * refresh the list of all tuples
	 * 
	 * @throws TuplespaceException
	 */
	private void refresh() throws TuplespaceException {
		if (space != null) {
			listModel_tuple = new DefaultListModel();
			List<Tree> entries;

			space.ensureAvailability();
			if (notifications.size() > 0) {
				entries = new LinkedList<Tree>();
				for (Notification notification : notifications) {
					Tree pattern = notification.getTemplate();
					entries.addAll(space.getDevice().scan(pattern));
				}
			} else
				entries = space.getDevice().scan();
			space.releaseAvailability();

			for (Tree entry : entries) {
				String s = new String(SExpressionWriter.writeSExpression(entry));
				listModel_tuple.addElement(s);
			}

			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					list_tuples.setModel(listModel_tuple);
				}
			});

		}
	}

	/**
	 * @return all Tuples this Panel is showing. Creates a new List.
	 * 
	 * @see #getTupleCount()
	 */
	public List<Tree> getTuples() {
		ArrayList<Tree> result = new ArrayList<Tree>(listModel_tuple.size());

		for (int i = 0; i < listModel_tuple.size(); i++)
			if (listModel_tuple.get(i) instanceof Tree)
				result.add((Tree) listModel_tuple.get(i));

		return result;
	}

	/**
	 * @return the number of Tuple this Panel is showing.
	 * 
	 * @see #getTuples()
	 */
	public int getTupleCount() {
		return listModel_tuple.size();
	}

	@Override
	public void addObserver(Observer<TuplespacePanelEvent> observer) {
		observers.add(observer);
	}

	@Override
	public void removeObserver(Observer<TuplespacePanelEvent> observer) {
		observers.remove(observer);
	}

	private void fireEvent(TuplespacePanelEvent event) {
		for (Observer<TuplespacePanelEvent> o : observers)
			o.notify(this, event);
	}

	@Override
	public Dimension getPreferredSize() {
		return list_tuples.getPreferredSize();
	}
}
